<?php

declare(ENCODING = 'UTF-8');

/*                                                                        *
 * This script is part of the TYPO3 project - inspiring people to share!  *
 *                                                                        *
 * TYPO3 is free software; you can redistribute it and/or modify it under *
 * the terms of the GNU General Public License version 2 as published by  *
 * the Free Software Foundation.                                          *
 *                                                                        *
 * This script is distributed in the hope that it will be useful, but     *
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHAN-    *	
 * TABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General      *
 * Public License for more details.                                       *
 *                                                                        */	

/**
 * @package auxo
 * @subpackage core
 * @version $Id$
 */

/**	
 * The Resource File Resolver
 * 
 * This class resolves one or more locations for a given set of resources. Resources are passed
 * as comma, space or semicolon separated string to method 'resolve' for resolving. A Resource 
 * could have a prefix which significates a certain location e.g. extension:/pi1/myFile.php. 
 * Moreover, resources might include placeholders which has to be substitute. Placeholders are 
 * defined as {placeholder} within a resource path e.g. /path/to/resource/{view}/index.tmpl.
 * Nevertheless, resources resolving supports wildcards in ant-style. Following patterns 
 * are possible e.g.:
 * 
 * <code>
 * path/file.php
 * /path/to/file.php
 * /path/to/file*.php
 * /path/to/*.php
 * /path/t?/*php
 * /path/*\/file.php 
 * /path/*\/modules/file.php  
 * /path/**\/file.php
 * *fi?e*.php
 * </code>
 * 
 * Resources passed with an relative path will be searched in a list of default directories. 
 * Currently, $home and PHP_INCLUDE_PATH are set as default locations.
 * 
 * <strong>Note: all pathes are handled in unix-style. Pathes passed over 
 * in windows style are sanitized automatically before use.</strong>
 *
 * @todo ant-pattern wilcard ** is not supported currently
 *
 * @package auxo
 * @subpackage core	
 * @version $Id$	
 * @copyright Copyright belongs to the respective authors
 * @author AHN
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License, version 2
 */
class  tx_auxo_ResourceFileResolver  {

	/**
	 * A home directory for all relative path searches
	 *
	 * @var string $home
	 */
	private $home = '';
	
	/**
	 * Default locations / pathes
	 * 
	 * @var array $locations
	 */	
	private $locations = array();
	
	/**
	 * An array of placeholder which represents a certain part of a URI
	 *
	 * @var array 
	 */
	private $placeholders = array();
	
	/**
	 * An array of prefixes which represents certain locations 
	 * in the filesystem
	 *
	 * @var array
	 */
	private $prefixes = array();
	
	/**
	 * Creates a new Resource File Resolver instance
	 *
	 * @param string $home home directory for relative path resolution
	 */
	public function __construct($home = '') {
		if ($home) $this->home = $this->setLocation($this->sanitizePath($home));
	}
	
	/**
	 * Resolves a given resources. Resources could be separated by comma, 
	 * semicolon or space. It returns an array of locations or throws an exception
	 * if all given resources could not be resolved.
	 *
	 * @param string $pattern
	 * @return array $locations array of filenames with fullpath
	 */
	public function resolve($pattern) {		
		$resources = $this->sanitizePath(preg_split('/[,; ]+/', $pattern, -1, PREG_SPLIT_NO_EMPTY));
		
		foreach($resources as $resource) {
			$resource = $this->substitutePrefixesAndPlaceholder($resource);
			$position = strrpos($resource, '/');
			if ($position === false) {
				$filename = $resource;
				$directories = $this->getDefaultDirectories();
			}
			else {
				$filename = substr($resource, $position + 1);
				$directories = $this->getMatchingDirectories($resource);				
			}

			foreach($directories as $directory) {
				$files = $this->matchFilesInDirectory($directory, $filename);				
			}
		}
		if (! isset($files)) {
			throw new tx_auxo_IOException(sprintf('Could not find any resource'));
		}
		
		return $files;
	}
	
	/**
	 * Adds a file prefix and its according path
	 *
	 * @param string $prefix prefix e.g. 'module'
	 * @param string $path path representing a prefix
	 */
	public function setPrefix($prefix, $path) {
        if (substr($path, 0, 4) == 'EXT:') {
		   $this->prefixes[$prefix] = t3lib_div::getFileAbsFileName($path, 1);
        }
        else {
		   $this->prefixes[$prefix] = $path;
        }
	}

	/**
	 * Removes a before set path prefix from this resolver
	 *
	 * @param string $prefix prefix e.g. 'module'
	 */
	public function removePrefix($prefix) {
		unset($this->prefixes[$prefix]);
	}
	
	/**
	 * Adds a placeholder and its according path snippet
	 *
	 * @param string $placeholder name of placeholder
	 * @param string $pathSnippet replacement
	 */
	public function setPlaceholder($placeholder, $pathSnippet) {
		$this->placeholders[$placeholder] = $pathSnippet;	
	}	
	
	/**
	 * Remove a placeholder from this resolver
	 *
	 * @param string $placeholder name of placeholder
	 */
	public function removePlaceholder($placeholder) {
		unset($this->placeholders[$placeholder]);
	}
	
	/**
	 * Adds a list of locations to resolver's default locations
	 *
	 * @param array $locations an array of valid pathes
	 */
	public function setLocationList(array $locations) {
		$this->locations = array_merge($this->loctions, $locations);
	}
	
	/**
	 * Adds a location to resolver's default locations
	 *
	 * @param string $location a valid path
	 */
	public function setLocation($location) {
		$this->locations[] = $location;
	}
	
	/**
	 * Removes a location from resolver's default locations
	 * 
	 * @param string $location a valid path
	 */
	public function removeLocation($location) {
		if (in_array($location, $this->locations)) {
			unset($this->locations[array_search($location, $this->locations)]);
		}
	}
	
	/**
	 * Substitutes Prefixes and Placholders
	 * 
	 * Pathes might be pre-fixed to point to a dedicated pre-defined 
	 * location e.g. EXT:auxo is equal ".../typo3conf/ext/auxo". Moreover,
	 * placeholder could be used which are replaced with before defined string 
	 * values.
	 * 
	 * Example: 
	 * 
	 * <code>
	 * $this->setPlaceholder('module', 'User');
	 * $path = $this->substitutePrefixesAndPlaceholder('path/to/{module}/action.php');
	 * echo $path -> path/to/User/action.php
	 * </code>
	 *
	 * @param string $path that includes prefix and/placeholders
	 * @return string $resolvedPath path with resolved prefix and substituted placeholders
	 * @throws tx_auxo_IOException paths starts with unknown prefix
	 */
	public function substitutePrefixesAndPlaceholder($path) {
		// check for prefixes
		if (strpos($path, ':')) {
			$prefix = substr($path, 0, strpos($path, ':'));
			/*
			 * A prefix of size 1 is interpreted as drive in windows style e.g. c:/path/to/...
			 * and therefore not substituted.
			 */
			if (strlen($prefix) <> 1) {
				if (! array_key_exists($prefix, $this->prefixes)) {
					throw new tx_auxo_pathPrefixUnknownException(sprintf('Path %s starts with unknown prefix %s', $path, $prefix));
				}
				$path = str_replace($prefix . ':', $this->prefixes[$prefix], $path);
			}
		}
		
		return $this->substitutePlaceholders($path);
	}

	/**
	 * Substitute placeholders in a path of format {placeholder}.
	 *
	 * @param string $path path that includes placeholders
	 * @return string $path path with substituted placeholders
	 * @throws tx_auxo_IOException paths contains unknown placeholder
	 */
	private function substitutePlaceholders($path) {
		if (! preg_match_all('/{([a-zA-Z0-0_]+)}/', $path, $matches)) {
			return $path;
		}

		foreach($matches[1] as $match) {
			if (! isset($this->placeholders[$match])) {			
				throw new tx_auxo_IOException(sprintf('Path %s contains unknown placeholder %s', $path, $match));
			}
			$path = str_replace('{' . trim($match) . '}', $this->placeholders[$match], $path);				
		}
		
		return $path;		
	}
		
	/**
	 * Returns an array of all matching directories for a given resource. if no 
	 * path is found the PHP default include pathes are returned as directory list.
	 *
	 * @param string $pattern resource 
	 * @return array $directories list of directories matching with $pattern
	 */
	private function getMatchingDirectories($pattern) {
		$directories = array();
		$position = strrpos($pattern, '/');
		$path = substr($pattern, 0, $position);

		if (! $this->isAbsolutePath($path)) {
			foreach($this->getDefaultDirectories() as $directory) {
				if (strlen($directory)) {
					$pathes[] = $directory. '/' . $path; 
				}
			}
		}
		else {
			$pathes[] = $path;
		}
		
		foreach ($pathes as $path) {
			if ($this->containsPattern($path)) {
				$directories = array_merge($directories, $this->matchDirectoryTree($path));
			}
			else {
				if (file_exists($path)) {
					$directories[] = $path;
				}
			}
		}
				
		return $directories;
	}	

	/**
	 * Returns true if a given path is absolute. This methods works in 
	 * windows as well as in unix enviroments.
	 *
	 * @param string $path
	 * @return boolean $absolute true if an absolute path is detected
	 */
	public function isAbsolutePath($path) {
		return preg_match('@(^[a-zA-Z]+:/|^/)@', $this->sanitizePath($path)) == 1;
	}
	
	/**
	 * Returns an array of default directories that are used in order to 
	 * resolve relative paths and/or filenames. It includes $home, $locations and 
	 * also PHP's default path. Pathes will be sanitized in unix-style and trailing
	 * path separators removed.
	 *
	 * @return array $directories list of default directories
	 */
	private function getDefaultDirectories() {			
		$directories = array_merge($this->locations, explode(';', $this->sanitizePath(get_include_path())));		
		foreach($directories as $key => $directory) {
			if (! is_readable($directory)) unset($directories[$key]);	
			// remove trailing path separator		
			$directories[$key] = rtrim($directory, '/');
		}
		
		return $directories;
	}
	
	/**
	 * Matches a path along the system directory tree and returns all matching
	 * pathes if readable. A path might contain wildcards like * or ? on different 
	 * levels of a directory hierarchy.
	 *
	 * @param string $path
	 * @return array $matches list of pathes that are readable and match $path
	 */
	private function matchDirectoryTree($path) {	
		if ($path[0] == '/') {
			// special treatment needed for absolute pathes in unix style
			$elements = explode('/', substr($path, 1));
			$elements[0] = '/' . $elements[0];
		}
		else {
			$elements = explode('/', $path);
		}
		
		$matches[] = $elements[0];
		array_shift($elements);

		foreach ($elements as $element) {
			unset($newMatches);
			$newMatches = array();
			foreach($matches as $match) {
				if (! $this->containsPattern($element)) {
					if (file_exists( $match . '/' . $element)) {
						$newMatches[] = $match . '/'. $element;
					}
				}
				else {
					if (count(($dirs = $this->getDirsInPath($match, $element)))) {
						foreach ($dirs as $dir) {
							$newMatches[] = $match . '/' . $dir;
						}
					}
				}
			}										
			$matches = $newMatches;
		}
		
		return $matches;
	}
	
	/**
	 * Returns true if string contains at least one * or ? wilcard character 
	 *
	 * @param string $string pattern to examine
	 * @return boolean $contains at least one pattern character found
	 */
	private function containsPattern($string) {
		return (strpos($string, '*') !== false or strpos($string, '?') !== false);
	}
	
	/**
	 * Returns an array of subordinated directories of an given path filtered 
	 * using parameter $filter. A filter might be used to get a subset of directories
	 * and can enclose wildcards * and/or ?.
	 *
	 * @param string $path path to a directory
	 * @param string $filter directory name with wildcards
	 * @return array $directories array of directories located in $path and matching $filter
	 */
	private function getDirsInPath($path, $filter) {
		$pattern = $this->getShellPattern($filter);
		
		$directories = array();
		$iterator = new DirectoryIterator($path);
		
		while($iterator->valid()) {
			if ($iterator->isDir() && strncmp($iterator->getFilename(), '.', 1)) {
				if ($filter == '' || $filter == '**') {
					$directories[] = $iterator->getFilename();		
				}
				elseif (preg_match($pattern, $iterator->getFilename())) {
					$directories[] = $iterator->getFilename();							
				}
			}
			
			$iterator->next();
		}		
		
		return $directories;
	}
		
	/**
	 * Returns an filtered array of files from a given path. A filter might be used 
	 * to get a subset of files and can enclose wildcards * and/or ?.
	 *
	 * @param string $path path 
	 * @param string $filter filter with wildcards ? or *
	 * @return array $files array of files that matches $filter
	 */
	private function matchFilesInDirectory($path, $filter) {
		if (! is_readable($path)) {
			throw new tx_auxo_IOException(sprintf('directory %s can not be read', $path));
		}
		
		$files = array();
		$pattern = $this->getShellPattern($filter);
		$iterator = new DirectoryIterator($path);
		
		while ($iterator->valid()) {
			if (! $iterator->isDir() && strncmp($iterator->getFilename(), '.', 1) != 0) {
				if (!$iterator->isReadable()) {
					throw new tx_auxo_IOException(sprintf('directory %s can not be read', $iterator->getPath()));
				}	            

				if (preg_match($pattern, (string) $iterator->getFilename())) {
					$files[] = $this->sanitizePath($iterator->getPathname());
				}
        	}
        	$iterator->next();
        }
		return $files;
	}

	/**
	 * Generates a preg pattern based on a typical shell pattern that is used
	 * to select files. Shell pattern can include * and ? as wildcards.
	 *
	 * @param string $string
	 * @return string $pattern
	 */
	private function getShellPattern($string) {
		$pattern = $string;
		foreach(str_split('+[]-$^.', 1) as $character) {
			$pattern = str_replace($character, '\\' . $character, $pattern);
		}
		if (substr($pattern, 0, 1) != '*') {
			$pattern = '^' . $pattern;
		}
		$pattern = str_replace('*', '[^*?]*', $pattern);
		return str_replace('?', '.', '@' . $pattern . '@');		
	}

	/**
	 * Sanitizes (transforms) a path in unix-style
	 *
	 * @param string $path a system-dependent path
	 * @return string $saniztizedPath a sanitized path in unix-style
	 */
	private function sanitizePath($path) {
		return str_replace('//', '/', str_replace('\\', '/', $path));
	}
}
?>